backend/feat: Stripe payout dynamic transfer top-up for Fleet VA insu…#1320
Conversation
…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>
WalkthroughThis 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. ChangesStripe Payout Merchant Balance Top-up
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
lib/mobility-core/src/Kernel/External/Payout/Interface.hs (1)
74-95: 🏗️ Heavy liftStranded 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) beforecreateExternalPayout(Line 78). Only the payout call is wrapped inwithTryCatch; on payout failure the flow returnsFAILUREbut 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
📒 Files selected for processing (5)
lib/mobility-core/src/Kernel/External/Payout/Interface.hslib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hslib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hslib/mobility-core/src/Kernel/External/Payout/Interface/Types.hslib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs
| when (merchantAvail < adjustedAmt) $ | ||
| throwError $ | ||
| InvalidRequest $ | ||
| "Merchant platform account has insufficient balance. Available: " | ||
| <> show merchantAvail | ||
| <> ", Required: " | ||
| <> show adjustedAmt |
There was a problem hiding this comment.
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.
| 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.
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.
Type of Change
Description
Additional Changes
Motivation and Context
How did you test it?
Checklist
./dev/format-all-files.shSummary by CodeRabbit