paid out amount updates on page load if webhook doesn't work
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user