paid out amount updates on page load if webhook doesn't work

This commit is contained in:
jackiettran
2026-01-05 23:13:18 -05:00
parent 8809a012d5
commit ec84b8354e

View File

@@ -293,6 +293,127 @@ class StripeWebhookService {
throw error;
}
}
/**
* Reconcile payout statuses for an owner by checking Stripe for actual status.
* This handles cases where the payout.paid webhook was missed or failed.
*
* Simplified approach: Since Stripe automatic payouts sweep the entire available
* balance, if there's been a paid payout after our transfer was created, our
* funds were included.
*
* @param {string} ownerId - The owner's user ID
* @returns {Object} - { reconciled, updated, errors }
*/
static async reconcilePayoutStatuses(ownerId) {
const results = {
reconciled: 0,
updated: 0,
errors: [],
};
try {
// Find rentals that need reconciliation
const rentalsToReconcile = await Rental.findAll({
where: {
ownerId,
payoutStatus: "completed",
stripeTransferId: { [Op.not]: null },
bankDepositStatus: { [Op.is]: null },
},
include: [
{
model: User,
as: "owner",
attributes: ["stripeConnectedAccountId"],
},
],
});
if (rentalsToReconcile.length === 0) {
return results;
}
logger.info("Reconciling payout statuses", {
ownerId,
rentalsCount: rentalsToReconcile.length,
});
// Get the connected account ID (same for all rentals of this owner)
const connectedAccountId = rentalsToReconcile[0].owner?.stripeConnectedAccountId;
if (!connectedAccountId) {
logger.warn("Owner has no connected account ID", { ownerId });
return results;
}
// Fetch recent paid payouts once for all rentals
const paidPayouts = await stripe.payouts.list(
{ status: "paid", limit: 20 },
{ stripeAccount: connectedAccountId }
);
if (paidPayouts.data.length === 0) {
logger.info("No paid payouts found for connected account", { connectedAccountId });
return results;
}
for (const rental of rentalsToReconcile) {
results.reconciled++;
try {
// Get the transfer to find when it was created
const transfer = await stripe.transfers.retrieve(rental.stripeTransferId);
// Find a payout that arrived after the transfer was created
const matchingPayout = paidPayouts.data.find(
(payout) => payout.arrival_date >= transfer.created
);
if (matchingPayout) {
await rental.update({
bankDepositStatus: "paid",
bankDepositAt: new Date(matchingPayout.arrival_date * 1000),
stripePayoutId: matchingPayout.id,
});
results.updated++;
logger.info("Reconciled rental payout status", {
rentalId: rental.id,
payoutId: matchingPayout.id,
arrivalDate: matchingPayout.arrival_date,
});
}
} catch (rentalError) {
results.errors.push({
rentalId: rental.id,
error: rentalError.message,
});
logger.error("Error reconciling rental payout status", {
rentalId: rental.id,
error: rentalError.message,
});
}
}
logger.info("Payout reconciliation complete", {
ownerId,
reconciled: results.reconciled,
updated: results.updated,
errors: results.errors.length,
});
return results;
} catch (error) {
logger.error("Error in reconcilePayoutStatuses", {
ownerId,
error: error.message,
stack: error.stack,
});
throw error;
}
}
}
module.exports = StripeWebhookService;