Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,14 @@ describe("Stripe Connect payout webhook route", () => {
);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ success: true });
// account.updated with charges+payouts enabled => "active"
// account.updated with charges+payouts enabled => "active", AND the raw
// capability booleans are persisted (#11172): the payout transfer gate reads
// payouts_enabled directly (defaults false), so storing status alone left
// every account non-payout-ready forever. The column must be written true.
expect(updateByAccountId).toHaveBeenCalledWith("acct_creator_1", {
status: "active",
charges_enabled: true,
payouts_enabled: true,
});
expect(emitAudit).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ async function handlePOST(c: AppContext): Promise<Response> {

await stripeConnectAccountsRepository.updateByAccountId(outcome.accountId, {
...(outcome.status ? { status: outcome.status } : {}),
// Persist the capability booleans, not just the derived status: the payout
// transfer gate reads `payouts_enabled` directly and it defaults false, so
// storing status alone left every account non-payout-ready forever (#11172).
...(outcome.chargesEnabled !== undefined
? { charges_enabled: outcome.chargesEnabled }
: {}),
...(outcome.payoutsEnabled !== undefined
? { payouts_enabled: outcome.payoutsEnabled }
: {}),
});
logger.info("[StripeConnect] webhook applied", {
type: event.type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,28 @@ describe("mapConnectWebhookEvent (#8922)", () => {
});
});

it("refreshes account status on account.updated", () => {
it("refreshes account status AND surfaces the capability booleans on account.updated (#11172)", () => {
const out = mapConnectWebhookEvent({
type: "account.updated",
account: "acct_1",
data: { object: { charges_enabled: true, payouts_enabled: true } },
});
expect(out.status).toBe("active");
// #11172: the booleans MUST be returned so the route persists them — the
// payout gate reads payouts_enabled directly (defaults false). Deriving
// status alone left every account non-payout-ready forever.
expect(out.chargesEnabled).toBe(true);
expect(out.payoutsEnabled).toBe(true);
});

it("surfaces false capabilities too (payouts not yet enabled → column stays false, truthfully) (#11172)", () => {
const out = mapConnectWebhookEvent({
type: "account.updated",
account: "acct_1",
data: { object: { charges_enabled: true, payouts_enabled: false } },
});
expect(out.chargesEnabled).toBe(true);
expect(out.payoutsEnabled).toBe(false);
});

it("ignores unrelated event types", () => {
Expand Down
17 changes: 15 additions & 2 deletions packages/cloud/shared/src/lib/services/stripe-connect-payout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ export interface ConnectWebhookOutcome {
payoutStatus?: ConnectPayoutStatus;
/** Account capability refresh, for `account.updated`. */
status?: StripeConnectStatus;
/**
* Raw capability booleans from `account.updated`. Persisted alongside `status`
* so the DB column reflects reality: the payout transfer gate reads
* `payouts_enabled` directly, and it defaults false — deriving only `status`
* from these and dropping the booleans left `payouts_enabled` false forever,
* rejecting every fiat payout (#11172).
*/
chargesEnabled?: boolean;
payoutsEnabled?: boolean;
/** True when the event type isn't one we act on. */
ignored: boolean;
}
Expand All @@ -175,12 +184,16 @@ export function mapConnectWebhookEvent(event: {
return { accountId: event.account, payoutStatus: "paid", ignored: false };
case "account.updated": {
const obj = event.data?.object ?? {};
const chargesEnabled = obj.charges_enabled === true;
const payoutsEnabled = obj.payouts_enabled === true;
return {
accountId: event.account,
status: connectStatusFromCapabilities({
charges_enabled: obj.charges_enabled === true,
payouts_enabled: obj.payouts_enabled === true,
charges_enabled: chargesEnabled,
payouts_enabled: payoutsEnabled,
}),
chargesEnabled,
payoutsEnabled,
ignored: false,
};
}
Expand Down
Loading