handling if owner disconnects their stripe account
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
|
||||
const logger = require("../utils/logger");
|
||||
const { parseStripeError, PaymentError } = require("../utils/stripeErrors");
|
||||
const { User } = require("../models");
|
||||
const emailServices = require("./email");
|
||||
|
||||
class StripeService {
|
||||
static async getCheckoutSession(sessionId) {
|
||||
@@ -111,6 +113,23 @@ class StripeService {
|
||||
|
||||
return transfer;
|
||||
} catch (error) {
|
||||
// Check if this is a disconnected account error (fallback for missed webhooks)
|
||||
if (this.isAccountDisconnectedError(error)) {
|
||||
logger.warn("Transfer failed - account appears disconnected", {
|
||||
destination,
|
||||
errorCode: error.code,
|
||||
errorType: error.type,
|
||||
});
|
||||
|
||||
// Clean up stale connection data asynchronously (don't block the error)
|
||||
this.handleDisconnectedAccount(destination).catch((cleanupError) => {
|
||||
logger.error("Failed to clean up disconnected account", {
|
||||
destination,
|
||||
error: cleanupError.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logger.error("Error creating transfer", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
@@ -119,6 +138,77 @@ class StripeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error indicates the connected account is disconnected.
|
||||
* Used as fallback detection when webhook was missed.
|
||||
* @param {Error} error - Stripe error object
|
||||
* @returns {boolean} - True if error indicates disconnected account
|
||||
*/
|
||||
static isAccountDisconnectedError(error) {
|
||||
// Stripe returns these error codes when account is disconnected or invalid
|
||||
const disconnectedCodes = ["account_invalid", "platform_api_key_expired"];
|
||||
|
||||
// Error messages that indicate disconnection
|
||||
const disconnectedMessages = [
|
||||
"cannot transfer",
|
||||
"not connected",
|
||||
"no longer connected",
|
||||
"account has been deauthorized",
|
||||
];
|
||||
|
||||
if (disconnectedCodes.includes(error.code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = (error.message || "").toLowerCase();
|
||||
return disconnectedMessages.some((msg) => message.includes(msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disconnected account - cleanup and notify.
|
||||
* Called as fallback when webhook was missed.
|
||||
* @param {string} accountId - The disconnected Stripe account ID
|
||||
*/
|
||||
static async handleDisconnectedAccount(accountId) {
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
where: { stripeConnectedAccountId: accountId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn("Cleaning up disconnected account (webhook likely missed)", {
|
||||
userId: user.id,
|
||||
accountId,
|
||||
});
|
||||
|
||||
// Clear connection
|
||||
await user.update({
|
||||
stripeConnectedAccountId: null,
|
||||
stripePayoutsEnabled: false,
|
||||
});
|
||||
|
||||
// Send notification
|
||||
await emailServices.payment.sendAccountDisconnectedEmail(user.email, {
|
||||
ownerName: user.firstName || user.name,
|
||||
hasPendingPayouts: true, // We're in a transfer, so there's at least one
|
||||
pendingPayoutCount: 1,
|
||||
});
|
||||
|
||||
logger.info("Sent account disconnected notification (fallback)", {
|
||||
userId: user.id,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
logger.error("Failed to clean up disconnected account", {
|
||||
accountId,
|
||||
error: cleanupError.message,
|
||||
});
|
||||
// Don't throw - let original error propagate
|
||||
}
|
||||
}
|
||||
|
||||
static async createRefund({
|
||||
paymentIntentId,
|
||||
amount,
|
||||
|
||||
Reference in New Issue
Block a user