fix(billing): payout fast-follow — deferred review polish from #498#519
fix(billing): payout fast-follow — deferred review polish from #498#519Gajesh2007 wants to merge 1 commit into
Conversation
The five items deferred (on record) from PR #498's final review round. None are money-correctness; all sit on already-graceful behavior: - status?refresh=1 gone-account unlink now clears ALL account-derived response fields (country/destination/instant), not just id/status - no stale snapshot for one page load. - instant payouts are rejected up front for recipient-agreement accounts: their transfers take +24h to become available, so the instant payout could never succeed - previously it failed at Stripe and fell back with fee churn. - the reconciler is schedule-aware: rows on weekly-payout accounts (JP) inside a 10-day bound log as normal transit instead of firing the stuck-account warning every hour. - the standard-withdraw success toast prefers the coordinator's schedule-aware message (weekly in Japan) over hard-coded 'daily' copy. - ready accounts get an 'Unlink Stripe account' action (voluntary country change) - previously only wedged/non-ready accounts had the CTA.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
These changes fall outside the current threat model's Files changed vs. threat model coverageNone of the seven changed files appear in any New trust-boundary surface introduced
These appear to add new coordinator-side endpoints for initiating Stripe Connect payouts and withdrawals. This touches the same trust boundary as TB-008 but from the payout/disbursement direction — which the current model does not cover (TB-008 focuses on inbound top-ups, not outbound disbursements). Key questions that should be answered before merging:
Reconciliation logic that reads Stripe-side state and updates the internal ledger is a new variant of TB-008. If it doesn't run inside a serializable transaction (or use a unique constraint on the Stripe transfer/payout ID), it introduces the same non-atomic idempotency gap as SEC-012 for the reconciliation path.
UI component driving payout flows. On its own this is low risk, but if it reads a coordinator URL from a user-controlled source and passes it as RecommendationAdd the following to the threat model:
A new threat entry covering outbound disbursement authorization (missing ownership check on payout recipient / amount) is warranted and is not currently captured by any T-0xx entry. 🔐 Threat model: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e01062559a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| writeJSON(w, http.StatusBadRequest, errorResponse("instant_unavailable", | ||
| "instant payouts aren't available for accounts in your country — standard delivery arrives via the automatic payout")) |
There was a problem hiding this comment.
Use a distinct code for country-based instant blocks
When a recipient-agreement account (AU/JP/etc.) already has a debit card and selects Instant, this new branch returns the same instant_unavailable code used for missing debit cards. The billing UI maps that code unconditionally to “Instant payouts need a debit card linked…” in console-ui/src/components/payouts/payout-copy.ts, so these users are told to add a debit card even though their country only supports standard and the raw backend message is never shown. Please use a distinct code or update the UI mapping for this reason so the recovery path is clear.
Useful? React with 👍 / 👎.
| if acct.PayoutInterval == "weekly" && | ||
| time.Since(st.oldest) < stripeStuckThresholdWeekly { |
There was a problem hiding this comment.
Limit weekly-transit grace to JP accounts
This skips the stuck warning for any account whose Stripe schedule is weekly, but the intentional weekly auto-schedule in this codebase is JP-only (billing.setAutoPayoutSchedule uses daily everywhere else). If a US/EU account is switched to weekly in Stripe or otherwise has that stale schedule, transferred withdrawals between 48h and 10d old are now logged as normal transit instead of surfacing the misconfiguration, delaying ops visibility while funds sit longer than the supported daily cadence. Please also check the account country (or the set of countries where daily is unavailable) before applying the weekly grace.
Useful? React with 👍 / 👎.
|
Found 1 test failure on Blacksmith runners: Failure
|
ethenotethan
left a comment
There was a problem hiding this comment.
Automated Code Review — Layr-Labs/d-inference#
Verdict: COMMENT
Security — ✅ No issues found
Performance — ✅ No issues found
Type_diligence — ✅ No issues found
Additive_complexity — ✅ No issues found
✅ All four passes clean. No issues found.
🤖 Automated review by Centaur · DAR-186

The five items deferred (on record, in-thread) from PR #498's final review round. None are money-correctness — each sits on behavior that already degraded gracefully — but together they remove fee churn, ops-alert noise, and stale/incorrect UI copy.
Behavior: before → after
flowchart TB subgraph Before A1[JP user withdraws standard] --> B1["toast: 'pays out daily' (wrong)<br/>reconciler warns hourly while<br/>funds are in normal weekly transit"] C1[recipient acct + debit card, instant] --> D1[debit -> transfer -> payouts.create FAILS<br/>fee refunded, sweep delivers<br/>= guaranteed fee churn] E1["status?refresh=1 finds account gone"] --> F1[auto-unlink, but response keeps stale<br/>country/destination for one page load] G1[ready acct, wrong country] --> H1[no self-serve unlink - support ticket] endflowchart TB subgraph After A2[JP user withdraws standard] --> B2["toast uses coordinator's schedule-aware<br/>message + weekly ETA; reconciler logs<br/>'normal weekly transit' until 10d bound"] C2[recipient acct + debit card, instant] --> D2["400 instant_unavailable up front<br/>(no debit, no fee, honest copy)"] E2["status?refresh=1 finds account gone"] --> F2[response clears ALL account-derived fields<br/>UI lands directly on fresh onboarding] G2[ready acct, wrong country] --> H2['Unlink Stripe account' action on the card] endCode: before → after
flowchart LR subgraph Before a1[stripe_withdraw.go<br/>instant gate: debit-card only] --> b1[stripe_reconcile.go<br/>single 48h threshold] c1[stripe_payouts.go refresh<br/>clears id/status only] --> d1[payout-copy.ts<br/>hard-coded 'daily' toast<br/>StripePayoutsCard: unlink only when non-ready] end subgraph After a2[stripe_withdraw.go<br/>+ recipient-agreement instant pre-gate] --> b2[stripe_reconcile.go<br/>+ stripeStuckThresholdWeekly 10d,<br/>oldest-row tracking per account] c2[stripe_payouts.go refresh<br/>clears country/destination/instant too] --> d2[payout-copy.ts<br/>prefers resp.message/eta<br/>StripePayoutsCard: unlink in ready branch] endTests
TestStripeWithdrawInstantRejectedOnRecipientAccount(400 before any debit)TestStripeStatusRefreshGoneAccountClearsResponseFields(response + store both clean)TestStripeReconcilerWeeklyScheduleInTransitNotWarned(info inside 10d, warn past it, weekly never "healed")Refs: deferred threads on #498 (comments 3525813193, 3525813196, 3525813199, 3525813204, 3525813209).
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.