Skip to content

backend/feat: stripe payout with transfer v2#1282

Open
roman-bodavskiy wants to merge 2 commits into
mainfrom
backend/feat/stripe-payout-with-transfer-v2
Open

backend/feat: stripe payout with transfer v2#1282
roman-bodavskiy wants to merge 2 commits into
mainfrom
backend/feat/stripe-payout-with-transfer-v2

Conversation

@roman-bodavskiy
Copy link
Copy Markdown
Contributor

@roman-bodavskiy roman-bodavskiy commented May 12, 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

  • New Features

    • Added Stripe transfer support with connected account capabilities
    • Enhanced payout processing to track transfer orders and status
  • Refactor

    • Updated refund operations with improved request/response handling
    • Modernized payment configuration with additional optional fields
    • Improved currency field handling in refund responses

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Walkthrough

This PR introduces Stripe transfer support to the payout system by decomposing payout order creation into two steps: fund transfer and external payout. It refactors the refund flow to accept structured request types, makes transfer amount and status observable in payout responses, and replaces PayoutStatus with ExternalPayoutStatus.

Changes

Transfer-Based Payout and Refund Flow Refactoring

Layer / File(s) Summary
Transfer types module and definitions
lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs
New TransferId (Text newtype), TransferReq (amount/currency/destination/metadata/description), and TransferObject (id/object/amount/created/currency/destination) with form encoding and JSON serialization.
Payout status type migration
lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs
Replaces PayoutStatus with ExternalPayoutStatus enum (EXTERNAL_PAYOUT_* variants) and updates PayoutObject.status field type, including JSON, HTTP query, schema, and Beam instances.
Payout interface and configuration types
lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs, lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs
Extends CreatePayoutOrderReq with transferAmount and Stripe fields (mConnectedAccountId, mExternalAccountId); extends CreatePayoutOrderResp with transferStatus and transferId; introduces TransferStatus enum, CreateExternalPayoutReq/Resp, TransferAccount, CreateTransferReq/Resp; adds platformAccountId to payout StripeConfig.
Refund interface and Stripe implementation
lib/mobility-core/src/Kernel/External/Payment/Interface.hs, lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs, lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs, lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs, lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs, lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs
Refund methods now accept request records; GetRefundResp.currency made optional; RefundObject.currency changed to Text; PaymentIntentReq fields application_fee_amount and transfer_data made optional with conditional form encoding; Stripe implementations compute amounts via usdToCents/eurToCents; add transferPaymentToConnectedAccount config flag; refactor platform-charge and connected-account payment intent construction.
Payout service implementation with transfer orchestration
lib/mobility-core/src/Kernel/External/Payout/Interface.hs, lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs, lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs
createPayoutOrder now validates connected account, creates transfer first, attempts external payout with error recovery, and merges results. payoutOrderStatus maps externalPayoutOrderStatus response with transfer context. New createTransfer function routes to Stripe or rejects Juspay. Juspay response builders set transfer fields to Nothing. Stripe introduces createExternalPayout (USD→cents conversion, metadata), externalPayoutOrderStatus (Stripe payout fetch and status mapping), and createTransfer (destination resolution with platform fallback).
Flow layer updates and module exposure
lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs, lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs, lib/mobility-core/mobility-core.cabal
Update ListPayoutsAPI and listPayouts to use ExternalPayoutStatus; add CreateTransferAPI and flow createTransfer function. Re-export Transfer module; expose it in cabal.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PayoutInterface
  participant StripeService
  participant StripeTransfer
  participant StripeExternalPayout
  Client->>PayoutInterface: createPayoutOrder(config, CreatePayoutOrderReq)
  PayoutInterface->>PayoutInterface: validate mConnectedAccountId
  PayoutInterface->>StripeService: createTransfer(StripeConfig, CreateTransferReq)
  StripeService->>StripeTransfer: Stripe.createTransfer
  StripeTransfer-->>StripeService: TransferObject
  StripeService-->>PayoutInterface: CreateTransferResp
  PayoutInterface->>StripeService: createExternalPayout(config, CreateExternalPayoutReq)
  StripeService->>StripeExternalPayout: Stripe.createPayout
  StripeExternalPayout-->>StripeService: ExternalPayoutResp
  StripeService-->>PayoutInterface: CreateExternalPayoutResp
  PayoutInterface->>PayoutInterface: merge transfer + external payout
  PayoutInterface-->>Client: CreatePayoutOrderResp
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nammayatri/shared-kernel#1221: Both PRs extend the Stripe payout surface; this PR's new Transfer types and external payout refactoring directly build on the payout module structure introduced in that PR.

Suggested reviewers

  • khuzema786

Poem

🐰 Hops with glee through transfer streams,
Payout dances, a new scheme!
Refund records, status bright—
Connected accounts, funds in flight!

🚥 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 'backend/feat: stripe payout with transfer v2' clearly and specifically describes the main change: implementation of Stripe payout functionality with transfer support (v2). The changeset confirms this with extensive modifications to payout and transfer handling across multiple modules, making it a strong match for the primary objective.
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/feat/stripe-payout-with-transfer-v2

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@roman-bodavskiy roman-bodavskiy self-assigned this May 12, 2026
@roman-bodavskiy roman-bodavskiy force-pushed the backend/feat/stripe-payout-with-transfer-v2 branch from c6c25a8 to bb9baf6 Compare May 12, 2026 10:14
@roman-bodavskiy roman-bodavskiy marked this pull request as ready for review May 12, 2026 13:08
@coderabbitai coderabbitai Bot requested a review from khuzema786 May 12, 2026 13:09
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 (4)
lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs (1)

58-58: 💤 Low value

Consider documenting why currency is now Text instead of Currency.

The currency field was changed from the strongly-typed Currency enum to Text, which reduces type safety at the JSON deserialization boundary. While the parsing now happens at the interface layer (line 807 in Interface/Stripe.hs), this allows potentially invalid currency values to be accepted during JSON decode.

Consider adding a comment explaining this design decision, or verify whether the Stripe API returns currency values that don't map cleanly to the internal Currency type.

🤖 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/Payment/Stripe/Types/Refund.hs` at line
58, Add an explanatory comment above the currency :: Text field in Refund.hs
that documents why the type was relaxed from the strong Currency enum to Text
(e.g., because Stripe can return values not representable by the internal
Currency type or to defer validation to the interface layer), and reference
where the parsing/validation now occurs (Interface/Stripe.hs around the
parse/validation logic at line ~807); alternatively, if the intent is to
preserve type safety, validate and map the Text to Currency during JSON decoding
or call out that validation is intentionally performed later in
Interface/Stripe.hs.
lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs (2)

343-345: 💤 Low value

Clarify the default behavior for transferPaymentToConnectedAccount.

The conditional logic at line 343-345:

(transfer_data, application_fee_amount) = case config.transferPaymentToConnectedAccount of
  Just False -> (Nothing, Nothing)
  _ -> (Just $ Stripe.TransferData {destination = driverAccountId}, Just $ eurToCents applicationFeeAmount)

The _ pattern means that both Nothing and Just True will set transfer data. Consider making the default explicit for clarity, or document this behavior if it's intentional.

♻️ Suggested clarification
-(transfer_data, application_fee_amount) = case config.transferPaymentToConnectedAccount of
-  Just False -> (Nothing, Nothing)
-  _ -> (Just $ Stripe.TransferData {destination = driverAccountId}, Just $ eurToCents applicationFeeAmount) -- True is default
+(transfer_data, application_fee_amount) = case fromMaybe True config.transferPaymentToConnectedAccount of
+  False -> (Nothing, Nothing)
+  True -> (Just $ Stripe.TransferData {destination = driverAccountId}, Just $ eurToCents applicationFeeAmount)
🤖 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/Payment/Interface/Stripe.hs` around
lines 343 - 345, The pattern matching for transfer_data and
application_fee_amount currently treats Nothing and Just True the same via `_`,
making the default behavior implicit; update the case on
config.transferPaymentToConnectedAccount to explicitly handle Just True and
Nothing separately (e.g., add a `Just True -> ...` branch and a `Nothing -> ...`
branch) or add a clarifying comment above the case to state the intended
default, referencing the symbols transfer_data, application_fee_amount,
config.transferPaymentToConnectedAccount, driverAccountId and eurToCents so the
behavior is explicit and easy to find.

339-339: ⚖️ Poor tradeoff

Use consistent currency conversion functions across all payment operations.

While the function implementations are identical (usdToCents and eurToCents both perform round $ money * 100), the inconsistent usage patterns are problematic for code clarity:

  • Lines 339, 345: Platform charge uses eurToCents on amountInUsd
  • Lines 372, 371: Connected account charge uses usdToCents on amountInUsd
  • Line 747: Refund uses eurToCents on amountInUsd

This suggests either (1) the actual currencies differ by operation but variable names are misleading, or (2) the function names don't reflect actual usage. Either way, standardize on a single function name across all similar operations to reduce confusion and maintenance burden.

🤖 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/Payment/Interface/Stripe.hs` at line
339, The code mixes eurToCents and usdToCents while operating on a value named
amountInUsd; standardize to a single conversion function (prefer usdToCents if
amounts are USD) by replacing all uses of eurToCents with usdToCents in the
platform charge, refund, and any other payment flows that accept amountInUsd
(e.g., the spots around the platform charge at the let amountInCents =
eurToCents amountInUsd, the connected account charge spots using usdToCents, and
the refund logic), or if some flows truly use EUR rename the amount/local
variables to reflect EUR and keep eurToCents—ensure the chosen function
(usdToCents) is the sole converter for USD flows and update callers and tests
accordingly.
lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs (1)

40-41: Track webhook and status-field implementation.

The TODO and comment note missing webhook handlers (transfer.created, transfer.reversed, transfer.updated) and the absence of a status field in TransferObject. Consider opening an issue to track this work.

Do you want me to open a new issue to track the webhook and status-field implementation?

🤖 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/Payout/Stripe/Types/Transfer.hs` around
lines 40 - 41, Open a new tracking issue for implementing Stripe transfer
webhooks and adding a status field to TransferObject referenced in
lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs; include
the TODO text as the issue description, list the specific events to handle
(transfer.created, transfer.updated, transfer.reversed), describe that current
Transfer API returns an error instead of an object on failure so status is
missing, specify required tasks (add status field to TransferObject, implement
webhook handlers, add tests and update docs), and tag with backend/payment and
priority labels so it can be scheduled.
🤖 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/Payout/Interface/Stripe.hs`:
- Around line 93-121: The createTransfer implementation misnames and mishandles
currency: buildCreateTransferReq uses amountInUsd and eurToCents regardless of
the CreateTransferReq.currency, and usdToCents/eurToCents are identical so
currency semantics are lost; update createTransfer (buildCreateTransferReq and
CreateTransferReq usage) to convert amounts based on the actual currency field
(not always EUR), either by replacing usdToCents/eurToCents with a single
currency-aware converter (e.g., amountToMinorUnits(currency, amount)) or by
switching on currency to call the correct conversion; also rename amountInUsd to
a neutral name (e.g., amount) if the request can be multiple currencies and
mirror the same currency-aware fix in createExternalPayout to keep behavior
consistent.

In `@lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs`:
- Around line 29-38: The ToForm instance for TransferReq currently omits the
metadata field; update the toForm (ToForm TransferReq) implementation to include
metadata when present by adding an entry to the catMaybes list that encodes the
Metadata into the form map (e.g. produce the appropriate key/value pairs for
each metadata entry and insert them into the HM.fromList used by Form). Locate
the toForm function for TransferReq and, when metadata :: Maybe Metadata is
Just, expand Metadata into the same form representation (using your existing
helpers such as toQueryParam or the pattern used for other nested form fields)
and append those key/value pairs so metadata is sent to Stripe.

---

Nitpick comments:
In `@lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs`:
- Around line 343-345: The pattern matching for transfer_data and
application_fee_amount currently treats Nothing and Just True the same via `_`,
making the default behavior implicit; update the case on
config.transferPaymentToConnectedAccount to explicitly handle Just True and
Nothing separately (e.g., add a `Just True -> ...` branch and a `Nothing -> ...`
branch) or add a clarifying comment above the case to state the intended
default, referencing the symbols transfer_data, application_fee_amount,
config.transferPaymentToConnectedAccount, driverAccountId and eurToCents so the
behavior is explicit and easy to find.
- Line 339: The code mixes eurToCents and usdToCents while operating on a value
named amountInUsd; standardize to a single conversion function (prefer
usdToCents if amounts are USD) by replacing all uses of eurToCents with
usdToCents in the platform charge, refund, and any other payment flows that
accept amountInUsd (e.g., the spots around the platform charge at the let
amountInCents = eurToCents amountInUsd, the connected account charge spots using
usdToCents, and the refund logic), or if some flows truly use EUR rename the
amount/local variables to reflect EUR and keep eurToCents—ensure the chosen
function (usdToCents) is the sole converter for USD flows and update callers and
tests accordingly.

In `@lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs`:
- Line 58: Add an explanatory comment above the currency :: Text field in
Refund.hs that documents why the type was relaxed from the strong Currency enum
to Text (e.g., because Stripe can return values not representable by the
internal Currency type or to defer validation to the interface layer), and
reference where the parsing/validation now occurs (Interface/Stripe.hs around
the parse/validation logic at line ~807); alternatively, if the intent is to
preserve type safety, validate and map the Text to Currency during JSON decoding
or call out that validation is intentionally performed later in
Interface/Stripe.hs.

In `@lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs`:
- Around line 40-41: Open a new tracking issue for implementing Stripe transfer
webhooks and adding a status field to TransferObject referenced in
lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs; include
the TODO text as the issue description, list the specific events to handle
(transfer.created, transfer.updated, transfer.reversed), describe that current
Transfer API returns an error instead of an object on failure so status is
missing, specify required tasks (add status field to TransferObject, implement
webhook handlers, add tests and update docs), and tag with backend/payment and
priority labels so it can be scheduled.
🪄 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: ab7b75e5-aa81-4388-a149-588be858ec6f

📥 Commits

Reviewing files that changed from the base of the PR and between 977129c and bb9baf6.

📒 Files selected for processing (16)
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/Payment/Interface.hs
  • lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs
  • lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs
  • lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs
  • lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs
  • lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs
  • lib/mobility-core/src/Kernel/External/Payout/Interface.hs
  • lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs
  • lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs
  • lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs
  • lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs
  • lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs
  • lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs
  • lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs
  • lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs

Comment on lines +93 to +121
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, transferStatus = TRANSFERRED}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check currency field usage and conversion patterns in payout/transfer code

# Search for currency conversion functions and their usage context
rg -n -C3 'usdToCents|eurToCents|centsToUsd' --type=hs -g '*Payout*' -g '*Payment*'

# Find where currency field from request is used
rg -n -C3 'currency.*=' --type=hs -g '*Payout/Interface*'

Repository: nammayatri/shared-kernel

Length of output: 217


🏁 Script executed:

cat -n lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs | head -150

Repository: nammayatri/shared-kernel

Length of output: 5726


🏁 Script executed:

rg -n 'createExternalPayout|usdToCents|eurToCents' --type=hs

Repository: nammayatri/shared-kernel

Length of output: 2804


🏁 Script executed:

fd -e hs | xargs grep -l 'createExternalPayout' 2>/dev/null | head -5

Repository: nammayatri/shared-kernel

Length of output: 191


🏁 Script executed:

rg -n 'data CreateTransferReq|data CreatePayoutOrderReq' --type=hs -A 10

Repository: nammayatri/shared-kernel

Length of output: 3293


🏁 Script executed:

rg -n 'type.*CreateTransferReq\|newtype.*CreateTransferReq' --type=hs -A 5

Repository: nammayatri/shared-kernel

Length of output: 50


🏁 Script executed:

fd -e hs | xargs grep -l 'CreateTransferReq' | head -3

Repository: nammayatri/shared-kernel

Length of output: 255


🏁 Script executed:

cat -n lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs | sed -n '56,75p; 144,155p'

Repository: nammayatri/shared-kernel

Length of output: 1331


🏁 Script executed:

rg -n 'Currency' --type=hs lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs | head -10

Repository: nammayatri/shared-kernel

Length of output: 127


🏁 Script executed:

rg -n 'buildCreateTransferReq\|CreateTransferReq' --type=hs lib/mobility-core/src/Kernel/External/Payout/ -B 2 -A 8

Repository: nammayatri/shared-kernel

Length of output: 50


🏁 Script executed:

rg -n 'data Currency|newtype Currency' --type=hs -A 15

Repository: nammayatri/shared-kernel

Length of output: 1768


🏁 Script executed:

rg -n 'createTransfer\|TransferReq' --type=hs lib/mobility-core/src/Kernel/External/Payout/ -B 3 -A 6

Repository: nammayatri/shared-kernel

Length of output: 50


🏁 Script executed:

rg -n 'Stripe.createTransfer' --type=hs -B 5 -A 5

Repository: nammayatri/shared-kernel

Length of output: 3378


🏁 Script executed:

cat -n lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs | sed -n '550,560p'

Repository: nammayatri/shared-kernel

Length of output: 472


Address inconsistent currency conversion and misleading parameter naming in createTransfer.

Both createExternalPayout and createTransfer have a currency field in their request types that supports INR, USD, and EUR, but both ignore it when converting amounts to cents:

  • createExternalPayout uses usdToCents (line 39)
  • createTransfer uses eurToCents (line 114) despite the parameter being named amountInUsd

Additionally, usdToCents and eurToCents have identical implementations (round $ money * 100), making the semantic distinction meaningless. While this works for USD and EUR (both use 2 decimal places), it will be incorrect if INR is actually used in production (INR uses 0 decimal places).

Recommend either:

  1. Use a single conversion function for consistency, or
  2. Make the conversion function respect the actual currency field from the request, or
  3. If the currency is always USD/EUR, remove the currency field or document why it's ignored
🤖 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/Payout/Interface/Stripe.hs` around
lines 93 - 121, The createTransfer implementation misnames and mishandles
currency: buildCreateTransferReq uses amountInUsd and eurToCents regardless of
the CreateTransferReq.currency, and usdToCents/eurToCents are identical so
currency semantics are lost; update createTransfer (buildCreateTransferReq and
CreateTransferReq usage) to convert amounts based on the actual currency field
(not always EUR), either by replacing usdToCents/eurToCents with a single
currency-aware converter (e.g., amountToMinorUnits(currency, amount)) or by
switching on currency to call the correct conversion; also rename amountInUsd to
a neutral name (e.g., amount) if the request can be multiple currencies and
mirror the same currency-aware fix in createExternalPayout to keep behavior
consistent.

Comment on lines +29 to +38
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
]
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 | ⚡ Quick win

Include metadata field in the ToForm encoding.

The TransferReq record includes a metadata :: Maybe Metadata field (Line 24), but the ToForm instance does not encode it into the form data. If metadata should be sent to Stripe, add it to the catMaybes list; otherwise, remove the field from the record.

🔧 Proposed fix to include metadata in form encoding
 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,
+            ("metadata",) . pure . toQueryParam <$> metadata,
             ("description",) . pure . toQueryParam <$> description
           ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
]
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,
("metadata",) . pure . toQueryParam <$> metadata,
("description",) . pure . toQueryParam <$> description
]
🤖 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/Payout/Stripe/Types/Transfer.hs` around
lines 29 - 38, The ToForm instance for TransferReq currently omits the metadata
field; update the toForm (ToForm TransferReq) implementation to include metadata
when present by adding an entry to the catMaybes list that encodes the Metadata
into the form map (e.g. produce the appropriate key/value pairs for each
metadata entry and insert them into the HM.fromList used by Form). Locate the
toForm function for TransferReq and, when metadata :: Maybe Metadata is Just,
expand Metadata into the same form representation (using your existing helpers
such as toQueryParam or the pattern used for other nested form fields) and
append those key/value pairs so metadata is sent to Stripe.

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