Skip to content

backend/feat: Stripe payout dynamic transfer top-up for Fleet VA insu…#1320

Merged
khuzema786 merged 1 commit into
mainfrom
backend/feat/stripe-payout-dynamic-transfer-topup
Jun 2, 2026
Merged

backend/feat: Stripe payout dynamic transfer top-up for Fleet VA insu…#1320
khuzema786 merged 1 commit into
mainfrom
backend/feat/stripe-payout-dynamic-transfer-topup

Conversation

@karan2cmd
Copy link
Copy Markdown
Member

@karan2cmd karan2cmd commented Jun 2, 2026

When a Fleet VA (Stripe connected account) has insufficient available balance to cover a payout, compute the shortfall and increase the platform → VA transfer accordingly. Verify the merchant platform account has enough before proceeding. Persist the extra top-up amount as merchantTopUpAmount in CreatePayoutOrderResp.

  • Stripe/Flow.hs: add BalanceFund, BalanceResp types and GET /v1/balance API
  • Interface/Stripe.hs: add getBalance wrapper and getAvailableForCurrency helper
  • Interface/Types.hs: add merchantTopUpAmount :: Maybe HighPrecMoney to CreatePayoutOrderResp
  • Interface.hs: balance-check + adjusted-transfer logic in Stripe createPayoutOrder path
  • Interface/Juspay.hs: set merchantTopUpAmount = Nothing in Juspay responses

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
    • Enhanced payout order creation with automatic balance adjustment. When fleet account balance is insufficient for the requested payout, the system validates and transfers additional funds from the merchant account if available. Payout responses now transparently report any merchant top-up amounts that were transferred to complete the payout.

…fficient balance

When a Fleet VA (Stripe connected account) has insufficient available balance
to cover a payout, compute the shortfall and increase the platform → VA transfer
accordingly. Verify the merchant platform account has enough before proceeding.
Persist the extra top-up amount as merchantTopUpAmount in CreatePayoutOrderResp.

- Stripe/Flow.hs: add BalanceFund, BalanceResp types and GET /v1/balance API
- Interface/Stripe.hs: add getBalance wrapper and getAvailableForCurrency helper
- Interface/Types.hs: add merchantTopUpAmount :: Maybe HighPrecMoney to CreatePayoutOrderResp
- Interface.hs: balance-check + adjusted-transfer logic in Stripe createPayoutOrder path
- Interface/Juspay.hs: set merchantTopUpAmount = Nothing in Juspay responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Walkthrough

This PR introduces merchant (platform) balance top-up capability for Stripe payouts. When a Fleet VA's available balance is insufficient for the requested payout, the system checks the merchant VA balance, computes a top-up amount, and uses an adjusted transfer amount. The change includes new Stripe balance API integration, query helpers, response type extension, and core adjustment logic across Stripe and Juspay implementations.

Changes

Stripe Payout Merchant Balance Top-up

Layer / File(s) Summary
Stripe balance endpoint contract and implementation
lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs
Introduces BalanceFund and BalanceResp types to model Stripe's /v1/balance response, defines the GetBalanceAPI Servant route, and implements getBalance function that builds an Euler client and calls Stripe's balance endpoint.
Balance query wrapper functions
lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs
Exports and implements getBalance (decrypts Stripe API key, delegates to Stripe flow layer) and getAvailableForCurrency (filters per-currency available balance entries and sums converted amounts into HighPrecMoney).
Create payout order response type extension
lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs
Adds optional merchantTopUpAmount field to CreatePayoutOrderResp record to carry the computed Stripe-specific top-up amount.
Stripe payout balance adjustment and wiring
lib/mobility-core/src/Kernel/External/Payout/Interface.hs
Implements balance checking in createPayoutOrder: queries fleet and merchant VA available balances, computes adjusted transfer amount if shortfall exists, validates merchant VA can cover top-up, and propagates the adjustment through transfer creation and response construction. Also sets merchantTopUpAmount = Nothing in payoutOrderStatus response.
Juspay response field propagation
lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs
Updates createPayoutOrder and payoutOrderStatus to populate merchantTopUpAmount = Nothing in constructed CreatePayoutOrderResp records.

Sequence Diagram

sequenceDiagram
  participant Caller as Payout Service
  participant StripeIntf as Interface.Stripe
  participant StripeFlow as Stripe Flow
  participant StripeAPI as Stripe API
  Caller->>StripeIntf: createPayoutOrder(amount)
  StripeIntf->>StripeFlow: getBalance(config)
  StripeFlow->>StripeAPI: GET /v1/balance
  StripeAPI-->>StripeFlow: BalanceResp (fleet available)
  StripeFlow-->>StripeIntf: BalanceResp
  StripeIntf->>StripeIntf: getAvailableForCurrency(Currency)
  StripeIntf->>StripeIntf: Compare fleet balance vs amount
  alt Shortfall exists
    StripeIntf->>StripeFlow: getBalance(config, merchantAcctId)
    StripeFlow->>StripeAPI: GET /v1/balance (merchant account)
    StripeAPI-->>StripeFlow: BalanceResp (merchant available)
    StripeFlow-->>StripeIntf: BalanceResp
    StripeIntf->>StripeIntf: Compute top-up amount
    StripeIntf->>StripeIntf: Validate merchant can cover
  end
  StripeIntf->>StripeAPI: POST /v1/transfers (adjusted amount)
  StripeAPI-->>StripeIntf: Transfer created
  StripeIntf->>Caller: CreatePayoutOrderResp (with merchantTopUpAmount)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • nammayatri/shared-kernel#1282: Both PRs modify the Stripe payout/transfer flow by changing how transfer amounts and response details are computed and included in payout responses.

Suggested reviewers

  • khuzema786
  • Vignesh-772

Poem

🐰 A rabbit hops with balance so fine,
Checking fleet and merchant align,
When one falls short, top-up takes flight,
Two VAs work in harmony 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 describes the main feature: implementing dynamic transfer top-up logic for Fleet VA when it has insufficient balance, with adjustments made via Stripe payout flow.
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-dynamic-transfer-topup

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 2, 2026 07:35
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: 1

🧹 Nitpick comments (1)
lib/mobility-core/src/Kernel/External/Payout/Interface.hs (1)

74-95: 🏗️ Heavy lift

Stranded top-up funds if the payout fails after the transfer succeeds.

createTransfer (Line 76) executes the platform→Fleet VA transfer (now potentially increased by the top-up) before createExternalPayout (Line 78). Only the payout call is wrapped in withTryCatch; on payout failure the flow returns FAILURE but the transferred top-up has already moved to the Fleet VA with no compensating reversal. The top-up logic amplifies this since the moved amount can now be materially larger. Please confirm there is a reconciliation/reversal path for the transfer in the failure branch.

Consider reversing the transfer (Stripe transfer reversal) or persisting the transferId for an async reconciliation job when the subsequent payout fails.

🤖 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.hs` around lines 74 -
95, The code calls Stripe.createTransfer (in the flow after
computeAdjustedTransfer) before calling Stripe.createExternalPayout and only
handles payout failure, which can leave the top-up transferred to the Fleet VA
without reversal; update the failure branch that handles createExternalPayout
(the pattern matching on result from withTryCatch "createExternalPayout") to
either (a) invoke a compensating reversal using the transfer id from
createTransferResp via Stripe's transfer reversal API when createExternalPayout
fails, or (b) persist the transfer id and related metadata (orderId, amount,
status) so an async reconciliation job can detect and refund/reverse the
transfer later; make the change around the createTransferResp /
createExternalPayout handling and ensure mkCreatePayoutOrderResp still receives
merchantTopUpAmount, createTransferResp and the constructed
CreateExternalPayoutResp.
🤖 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.hs`:
- Around line 58-64: Replace the client-facing error that includes merchantAvail
and adjustedAmt with a generic InvalidRequest message (e.g., "Merchant platform
account has insufficient balance") while logging the sensitive values
internally; specifically, in the block that currently calls throwError (using
InvalidRequest and referencing merchantAvail and adjustedAmt), emit an internal
log entry containing merchantAvail, adjustedAmt and any request identifiers,
then call throwError with the generic message so the API response does not leak
balances.

---

Nitpick comments:
In `@lib/mobility-core/src/Kernel/External/Payout/Interface.hs`:
- Around line 74-95: The code calls Stripe.createTransfer (in the flow after
computeAdjustedTransfer) before calling Stripe.createExternalPayout and only
handles payout failure, which can leave the top-up transferred to the Fleet VA
without reversal; update the failure branch that handles createExternalPayout
(the pattern matching on result from withTryCatch "createExternalPayout") to
either (a) invoke a compensating reversal using the transfer id from
createTransferResp via Stripe's transfer reversal API when createExternalPayout
fails, or (b) persist the transfer id and related metadata (orderId, amount,
status) so an async reconciliation job can detect and refund/reverse the
transfer later; make the change around the createTransferResp /
createExternalPayout handling and ensure mkCreatePayoutOrderResp still receives
merchantTopUpAmount, createTransferResp and the constructed
CreateExternalPayoutResp.
🪄 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: 6b4a0eb8-aea5-40f3-803d-a0d3fffadf66

📥 Commits

Reviewing files that changed from the base of the PR and between d2d372d and c78e78c.

📒 Files selected for processing (5)
  • 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/Flow.hs

Comment on lines +58 to +64
when (merchantAvail < adjustedAmt) $
throwError $
InvalidRequest $
"Merchant platform account has insufficient balance. Available: "
<> show merchantAvail
<> ", Required: "
<> show adjustedAmt
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 leaking the platform account balance in the client-facing error.

merchantAvail is the platform/merchant VA's total available balance for the currency. Embedding it (and the required amount) in an InvalidRequest that propagates to the API caller discloses sensitive internal financial state. Log the figures internally and return a generic message to the client.

🛡️ Proposed fix
-              when (merchantAvail < adjustedAmt) $
-                throwError $
-                  InvalidRequest $
-                    "Merchant platform account has insufficient balance. Available: "
-                      <> show merchantAvail
-                      <> ", Required: "
-                      <> show adjustedAmt
+              when (merchantAvail < adjustedAmt) $ do
+                logError $
+                  "Merchant platform account has insufficient balance. Available: "
+                    <> show merchantAvail
+                    <> ", Required: "
+                    <> show adjustedAmt
+                    <> " orderId: " <> req.orderId
+                throwError $ InvalidRequest "Merchant platform account has insufficient balance to cover the payout top-up."
📝 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
when (merchantAvail < adjustedAmt) $
throwError $
InvalidRequest $
"Merchant platform account has insufficient balance. Available: "
<> show merchantAvail
<> ", Required: "
<> show adjustedAmt
when (merchantAvail < adjustedAmt) $ do
logError $
"Merchant platform account has insufficient balance. Available: "
<> show merchantAvail
<> ", Required: "
<> show adjustedAmt
<> " orderId: " <> req.orderId
throwError $ InvalidRequest "Merchant platform account has insufficient balance to cover the payout top-up."
🤖 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.hs` around lines 58 -
64, Replace the client-facing error that includes merchantAvail and adjustedAmt
with a generic InvalidRequest message (e.g., "Merchant platform account has
insufficient balance") while logging the sensitive values internally;
specifically, in the block that currently calls throwError (using InvalidRequest
and referencing merchantAvail and adjustedAmt), emit an internal log entry
containing merchantAvail, adjustedAmt and any request identifiers, then call
throwError with the generic message so the API response does not leak balances.

@khuzema786 khuzema786 merged commit 1e3abfa into main Jun 2, 2026
2 checks passed
prakharritik pushed a commit that referenced this pull request Jun 2, 2026
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.

2 participants