Skip to content

fix(billing): payout fast-follow — deferred review polish from #498#519

Open
Gajesh2007 wants to merge 1 commit into
masterfrom
fix/stripe-payout-fast-follow
Open

fix(billing): payout fast-follow — deferred review polish from #498#519
Gajesh2007 wants to merge 1 commit into
masterfrom
fix/stripe-payout-fast-follow

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Jul 5, 2026

Copy link
Copy Markdown
Member

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]
  end
Loading
flowchart 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]
  end
Loading

Code: 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]
  end
Loading

Tests

  • TestStripeWithdrawInstantRejectedOnRecipientAccount (400 before any debit)
  • TestStripeStatusRefreshGoneAccountClearsResponseFields (response + store both clean)
  • TestStripeReconcilerWeeklyScheduleInTransitNotWarned (info inside 10d, warn past it, weekly never "healed")
  • JP/daily toast pins in payout-copy.test.ts
  • Full coordinator suite + UI lint/vitest green.

Refs: deferred threads on #498 (comments 3525813193, 3525813196, 3525813199, 3525813204, 3525813209).


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

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.
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jul 5, 2026 11:42pm
d-inference-console-ui-dev Ready Ready Preview Jul 5, 2026 11:42pm
d-inference-landing Ready Ready Preview Jul 5, 2026 11:42pm

Request Review

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

These changes fall outside the current threat model's affected_files coverage but introduce meaningful new attack surface that should be added.


Files changed vs. threat model coverage

None of the seven changed files appear in any affected_files list. The closest existing coverage is TB-008 / T-030 (Stripe webhook double-credit, billing_handlers.go) and T-031 (unauthenticated earnings endpoint, consumer.go). The new files are distinct handler modules that didn't exist when the threat model was written.


New trust-boundary surface introduced

coordinator/api/stripe_payouts.go and stripe_withdraw.go

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:

  • Authentication: Are these endpoints gated by requireAuth or the admin middleware? An unauthenticated or consumer-authenticated payout-initiation endpoint would be a critical finding analogous to T-031 (SEC-030).
  • Idempotency: T-030 / SEC-012 documents that the existing webhook credit path is non-atomic. If stripe_withdraw.go follows the same read-then-write pattern for withdrawal state, it inherits the same double-spend risk — in the outbound direction, meaning funds leave the platform twice.
  • Amount/recipient validation: Any endpoint that calls the Stripe API to create a payout must validate that the amount and destination account ID come from coordinator-owned state, not a consumer-supplied request body. A missing ownership check here is an authorization bypass (BOLA/IDOR).

coordinator/api/stripe_reconcile.go

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.

console-ui/src/components/payouts/StripePayoutsCard.tsx

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 x-coordinator-url to a BFF route that handles payouts, it would extend the existing SSRF surface (T-017 / SEC-001) to cover outbound payment calls — higher impact than telemetry or status calls.


Recommendation

Add the following to the threat model:

Suggested new affected_files entries Relevant existing threats to extend
coordinator/api/stripe_payouts.go TB-008, T-030, T-031
coordinator/api/stripe_withdraw.go TB-008, T-030
coordinator/api/stripe_reconcile.go TB-008, T-030
console-ui/src/components/payouts/StripePayoutsCard.tsx TB-004, T-017

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: docs/threat-model.yaml · Updates on each push to this PR

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +155 to +156
writeJSON(w, http.StatusBadRequest, errorResponse("instant_unavailable",
"instant payouts aren't available for accounts in your country — standard delivery arrives via the automatic payout"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +143 to +144
if acct.PayoutInterval == "weekly" &&
time.Since(st.oldest) < stripeStuckThresholdWeekly {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@blacksmith-sh

blacksmith-sh Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
TestIntegration_ConcurrentRequests View Logs

Fix with Codesmith
Need help on this PR? Tag /codesmith with what you need.

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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